<?php

/**
 * MNet Library 
 * Copyright (C) 2006-2008 Catalyst IT Ltd (http://www.catalyst.net.nz)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package    mnet
 * @subpackage core
 * @author     Catalyst IT Ltd
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
 * @copyright  (C) 2006-2008 Catalyst IT Ltd http://catalyst.net.nz
 * @copyright  (C) portions from Moodle, (C) Martin Dougiamas http://dougiamas.com
 *
 */


/**
 * This class represents a peer with which we are communicating. The
 * host application should keep a database of peers and their public
 * keys.
 * 
 * You may find it useful to extend this class within your application
 * to keep track of other data, for example you can keep track of when
 * a peer is used by overriding the get_public_key() and updating a
 * timestamp variable, etc.
 */
class mnet_peer {
    protected $wwwroot     = '';
    protected $public_key  = '';
    protected $server_path = '/mnet/xmlrpc/server.php';
    protected $protocol    = 1.0;

    /**
     * Blank constructor
     */
    public function __construct() {
        // does nothing right now
    }

    /**
     * This method is for populating an mnet_peer record from a database or other data
     * source in response to an mnet_application::get_peer_by_wwwroot() call
     * 
     * @param string $wwwroot the wwwroot (and primary key) of the peer
     * @param string $public_key the X.509 certificate for the peer, if known
     * @param string $server_path the path, when appended to the wwwroot, that has an mnet_server-backed script for handling incoming requests (eg. /mnet/server.php)
     * @param float  $protocol the protocol version for this peer last time, or null if unknown
     * @return mnet_peer the populated peer record
     */
    public static function populate($wwwroot, $public_key, $server_path, $protocol = null) {
        // we fetch the name of our class in case the application is calling populate()
        // on a class derived from mnet_peer, not mnet_peer itself
        $class = get_class();
        $peer = new $class();

        $peer->wwwroot = $wwwroot;
        $peer->public_key = $public_key;
        $peer->server_path = $server_path;
        if ($protocol) {
            $peer->protocol = $protocol;
        }

        return $peer;
    }

    /**
     * Fetches or creates a peer record based on the provided wwwroot. If the
     * wwwroot is already known, we just return that one, otherwise we create
     * a new one using the provided $pubkey and $application. If the pubkey
     * is not supplied, we'll call out to them to ask for it, although note
     * that we cannot verify that the public key we get back is authentic.
     * 
     * You should prefer to create/bootstrap new peers from your application's
     * backend, which may also use this method when creating a new peer.
     *
     * The peer will be saved back.
     *
     * @param string $wwwroot - address of peer whose details we want
     * @param string $pubkey - to use if we add a record to db for new peer
     * @param string $application - table id - what kind of peer are we talking to
     * @param float  $protocol the protocol version the client called with (use 1.0 if nothing was supplied)
     * @return mnet_peer the new or existing mnet_peer record if successful, or false if there was a problem (will be logged)
     */
    public static function bootstrap($wwwroot, $pubkey = null, $server_path = null, $protocol = null) {
        global $MNET_APPLICATION, $MNET;

        // Remove the trailing slash, if present
        if (substr($wwwroot, -1, 1) == '/') {
            $wwwroot = substr($wwwroot, 0, -1);
        }

        // Check if a peer record already exists for this wwwroot
        if ($peer = $MNET_APPLICATION->get_peer_by_wwwroot($wwwroot)) {
          
        }
        else {
          // we fetch the name of our class in case the application is calling populate()
          // on a class derived from mnet_peer, not mnet_peer itself
          $class = get_class();
          $peer = new $class();
          if (!$server_path) {
            $peer->server_path = '/mnet/xmlrpc/server.php';
          }
          if (!$protocol) {
            $peer->protocol = 1.0;
          }
        }

        $peer->wwwroot = $wwwroot;
        if ($server_path) {
          $peer->server_path = $server_path;
        }
        if ($protocol) {
          $peer->protocol = $protocol;
        }

        if (empty($pubkey) && empty($peer->public_key)) {
            $peer->fetch_public_key();
        } else if ($pubkey) {
            $peer->set_public_key($MNET_APPLICATION->clean_pem($pubkey));
        }

        if (!$peer->has_public_key()) {
            $MNET_APPLICATION->log(MNET_LOG_ERR, 'Unable to get pubkey cert for '.$peer);
            return false;
        }

        $cert_info = $MNET->parse_certificate($peer->get_public_key());

        if ($cert_info['wwwroot'] != $wwwroot) {
            $MNET_APPLICATION->log(MNET_LOG_ERR, 'Certificate for '.$wwwroot.' is actually for wwwroot '.$cert_info['wwwroot']);
            return false;
        }

        $MNET_APPLICATION->log(MNET_LOG_NOTICE, 'Bootstrapping new peer with '.$wwwroot.' pubkey '.$pubkey);

        $peer->commit();
        return $peer;
    }

    /**
     * Refresh the peer's public key. Note this is an insecure method.
     * 
     * Fills in $this->public_key
     *
     */
    public function fetch_public_key() {
        global $MNET_APPLICATION;

        $system = mnet_extension::get('system');
        $this->public_key = $MNET_APPLICATION->clean_pem($system->get_remote_public_key($this));
        return $this->public_key;
    }

    /**
     * Returns just the domain of this peer, using the wwwroot
     *
     * @return string the domain part of the wwwroot
     */
    public function get_domain() {
        return parse_url($this->wwwroot, PHP_URL_HOST);
    }

    /**
     * Returns the path (from the wwwroot) to the XML-RPC request
     * handler
     * 
     * @return string the URL path (after the wwwroot) to the XML-RPC server
     */
    public function get_server_path() {
        return $this->server_path;
    }

    /**
     * Returns the UNIX timestamp when the currently configured public key certificate expires
     * 
     * @return int the expiry time, or false on error or if there's no known pubkey cert for this peer
     */
    public function get_key_expires() {
        global $MNET;

        return $MNET->get_certificate_expiry($this->public_key);
    }

    /**
     * Save a peer record back to persistent storage
     */
    public function commit() {
        global $MNET_APPLICATION;

        $MNET_APPLICATION->save_peer($this);
    }

    /**
     * Returns true if a public key has been set for this peer
     * 
     * @return bool true if a key is set
     */
    public function has_public_key() {
        return !empty($this->public_key);
    }

    /**
     * Returns the public key certificate for this peer (if known)
     * 
     * @return string the public key certificate, as a string
     */
    public function get_public_key() {
        return $this->public_key;
    }

    /**
     * Set a new public key for this peer
     *
     * @param string $public_key the new public key certificate in X.509 format
     */
    public function set_public_key($public_key) {
        $this->public_key = $public_key;
    }
    
    /**
     * Returns the wwwroot of this peer (primary key)
     * 
     * @return string the wwwroot
     */
    public function get_wwwroot() {
        return $this->wwwroot;
    }
    
    /**
     * Returns the protocol version last observed for this peer
     * 
     * @return float the protocol version
     */
    public function get_protocol() {
        return (float)$this->protocol;
    }

    /**
     * Update the protocol version for this peer 
     * 
     * @param float $version the new protocol version
     */
    public function set_protocol($version) {
        $this->protocol = (float)$version;
    }
    
    /**
     * Update the server path (path to the XML-RPC request handler)
     * for this peer. This is called automatically from mnet_server
     * and mnet_client to update the path if it's wrong. This is useful
     * for autodetecting a new client's path
     *
     * @param string $path the new server path
     */
    public function set_server_path($path) {
        $this->server_path = $path;
    }

    /**
     * Magic method to stringify a peer (eg. for logging purposes)
     * 
     * @return string a string representation of this peer
     */
    public function __toString() {
        return $this->wwwroot;
    }

    /**
     * Call this when you suspect/know that the public key on file for this
     * peer is wrong and you would like to get a newer copy of the key
     * by a trusted means. This works by initiating a 'ping' request back towards
     * the client. If the key we use to encrypt our ping request is old, the client
     * will send us back a "7025" XML-RPC fault, which the XML-RPC client will
     * use to update the public key on record for this peer with.
     * If this method returns true, you can be sure that we have the latest public
     * key for the client.
     * 
     * The public key will automatically be reloaded in this instance following this
     * call.
     * 
     * @return bool true if we succeeded in checking the client's public key
     */
    public function check_for_key_update() {
        global $MNET_APPLICATION;

        $system = mnet_extension::get('system');
        if ($system->ping($this)) {
            // Reload this peer record
            $ourself = $MNET_APPLICATION->get_peer_by_wwwroot($this->wwwroot);
            $this->set_public_key($ourself->public_key);
        }
    }
}

